<!--
 * @author  dumeng
 * @desc 附件上传
-->
<template>
  <div class="browser-upload-main" :class="{ hide: hide }">
    <div class="main-mask" />
    <div class="main-inner">
      <div class="inner-hide">
        <el-tooltip
          class="item"
          effect="dark"
          content="最小化"
          placement="top-start"
        >
          <i class="el-icon-minus" @click="handleHide" />
        </el-tooltip>
        <el-tooltip
          class="item"
          effect="dark"
          content="关闭"
          placement="top-start"
        >
          <i class="el-icon-close" @click="handleClose" />
        </el-tooltip>
      </div>
      <div class="inner-progress">
        <div class="btns">
          <el-button-group>
            <el-button :disabled="changeDisabled">
              <i class="el-icon-upload2 el-icon--left" size="mini" />选择文件
              <input
                v-if="!changeDisabled"
                type="file"
                :multiple="multiple"
                class="select-file-input"
                :accept="accept"
                οnclick="f.outerHTML=f.outerHTML"
                @change="handleFileChange"
              >
            </el-button>
            <el-button
              :disabled="uploadDisabled"
              :class="{ 'linear-btn': !uploadDisabled }"
              @click="handleUpload"
            >
              <i class="el-icon-upload el-icon--left" size="mini" />上传
            </el-button>
            <el-button :disabled="pauseDisabled" @click="handlePause">
              <i class="el-icon-video-pause el-icon--left" size="mini" />暂停
            </el-button>
            <el-button :disabled="resumeDisabled" @click="handleResume">
              <i class="el-icon-video-play el-icon--left" size="mini" />恢复
            </el-button>
            <el-button :disabled="clearDisabled" @click="clearFiles">
              <i class="el-icon-delete el-icon--left" size="mini" />清空
            </el-button>
          </el-button-group>
          <slot name="header" />
        </div>
      </div>
      <div class="inner-list">
        <el-collapse v-if="uploadFiles.length" accordion>
          <el-collapse-item v-for="(item, index) in uploadFiles" :key="index">
            <template slot="title">
              <div class="progress-box">
                <div class="list-item">
                  <div class="item-name">
                    <span>{{ index + 1 }}：名称：{{ item.name }}</span>
                  </div>
                  <div class="item-size">
                    大小：{{ item.size | transformByte }}
                  </div>
                  <div v-if="item.hashProgress !== 100" class="item-progress">
                    <span>{{
                      status === "read" ? "正在读取文件" : "正在生成切片"
                    }}：</span>
                    <el-progress :percentage="item.hashProgress" />
                  </div>
                  <div v-else class="item-progress">
                    <span>上传文件进度：</span>
                    <el-progress :percentage="item.uploadProgress" />
                  </div>
                  <div class="item-status">
                    <i :class="fileStatuIcon(item.status)" />
                    {{ item.status | fileStatus }}
                  </div>
                </div>
              </div>
            </template>
            <div v-if="showChunks" class="item-chunk-box">
              <el-table :data="item.chunks" border max-height="300">
                <el-table-column prop="index" label="切片#" align="center" />
                <el-table-column label="大小" align="center" width="120">
                  <template v-slot="{ row }">{{
                    row.size | transformByte
                  }}</template>
                </el-table-column>
                <el-table-column
                  prop="uploaded"
                  label="是否完成"
                  align="center"
                >
                  <template v-slot="{ row }">{{
                    row.uploaded ? "完成" : "进行中"
                  }}</template>
                </el-table-column>

                <el-table-column label="进度" align="center">
                  <template v-slot="{ row }">
                    <el-progress
                      v-if="!row.status || row.status === 'wait'"
                      :percentage="row.progress"
                    />
                    <el-progress
                      v-else
                      :percentage="row.progress"
                      :status="row.status"
                    />
                  </template>
                </el-table-column>
              </el-table>
            </div>
          </el-collapse-item>
        </el-collapse>
        <slot name="tip" />
      </div>
    </div>
    <div class="main-small" @click="handleHide">
      <el-tooltip
        class="item"
        effect="dark"
        content="展开文件上传"
        placement="top"
      >
        <i class="el-icon-upload" />
      </el-tooltip>
    </div>
  </div>
</template>
<script>
import axios, { CancelToken } from 'axios'
import Qs from 'qs'
const sha256 = require('js-sha256').sha256
var instance = axios.create({
  transformRequest: [(data) => {
    data = Qs.stringify(data)
    return data
  }]
})

// 所有文件状态
const Status = {
  read: 'read',
  wait: 'wait',
  pause: 'pause',
  uploading: 'uploading',
  hash: 'hash',
  error: 'error',
  done: 'done'
}

// 单个文件的状态
const fileStatus = {
  read: 'read',
  wait: 'wait',
  uploading: 'uploading',
  success: 'success',
  error: 'error',
  secondPass: 'secondPass',
  pause: 'pause',
  resume: 'resume'
}

// 单个文件的状态 对应描述
const fileStatusStr = {
  read: '文件读取',
  wait: '待上传',
  uploading: '上传中',
  success: '成功',
  error: '失败',
  secondPass: '已秒传',
  pause: '暂停',
  resume: '恢复'
}

export default {
  name: 'BrowserUploader',
  filters: {
    transformByte(size) {
      if (!size) {
        return '0B'
      }

      var num = 1024.0 // byte

      if (size < num) {
        return size + 'B'
      }
      if (size < Math.pow(num, 2)) {
        return (size / num).toFixed(2) + 'K'
      } // kb
      if (size < Math.pow(num, 3)) {
        return (size / Math.pow(num, 2)).toFixed(2) + 'M'
      } // M
      if (size < Math.pow(num, 4)) {
        return (size / Math.pow(num, 3)).toFixed(2) + 'G'
      } // G
      return (size / Math.pow(num, 4)).toFixed(2) + 'T' // T
    },
    // 单个文件状态格式化
    fileStatus(status) {
      return fileStatusStr[fileStatus[status]]
    }
  },
  props: {
    headers: {
      type: Object,
      default: null
    },
    beforeUpload: {
      type: Function,
      default: null
    },
    // 支持文件类型
    accept: {
      type: String,
      default: ''
    },
    // 上传文件时携带的参数
    uploadArguments: {
      type: Object,
      default: () => {
        return {}
      }
    },
    // 是否传递cookie
    withCredentials: {
      type: Boolean,
      default: false
    },
    // 文件个数
    limit: {
      type: Number,
      default: 1,
      validator: (val) => {
        return val <= 2
      }
    },
    // 文件超出个数限制时的钩子
    onExceed: {
      type: Function,
      default: () => { }
    },
    // 多文件上传
    multiple: {
      type: Boolean,
      default: true
    },
    // axios baseUrl
    baseUrl: {
      type: String,
      default: process.env.BASE_API
    },
    // 切片大小
    chunkSize: {
      type: Number,
      default: 10 * 1024 * 1024,
      validator: (val) => {
        return val <= (10 * 1024 * 1024)
      }
    },
    // 上传并发数
    threads: {
      type: Number,
      default: 2,
      validator: (val) => {
        return val <= 4
      }
    },
    // 错误重试次数
    chunkRetry: {
      type: Number,
      default: 3
    },
    // 是否显示切片上传进度
    showChunks: {
      type: Boolean,
      default: false
    },
    isOss: {
      type: [String, Boolean],
      required: false,
      default: ''
    }
  },
  data: () => ({
    uploadFiles: [],
    fileIndex: 0, // 当前正在被遍历的文件下标
    worker: null,
    cancels: [], // 存储要取消的请求
    _threads: 2,
    status: Status.read, // 默认状态
    hide: false,
    _axiosworker: null
  }),
  computed: {
    changeDisabled() {
      return ![Status.read, Status.wait, Status.done].includes(this.status)
    },
    uploadDisabled() {
      return [Status.read, Status.pause, Status.done, Status.uploading, Status.hash].includes(this.status)
    },
    pauseDisabled() {
      return [Status.read, Status.hash, Status.select, Status.wait, Status.done, Status.pause].includes(this.status)
    },
    resumeDisabled() {
      return ![Status.pause].includes(this.status)
    },
    clearDisabled() {
      return !this.uploadFiles.length
    },
    fileStatuIcon(status) {
      return (status) => {
        var className = ''
        switch (status) {
          case 'uploading':
            className = 'el-icon-loading'
            break
          case 'success':
          case 'secondPass':
            className = 'el-icon-circle-check'
            break
          case 'error':
            className = 'el-icon-circle-close'
            break
        }
        return className + ' el-icon--left'
      }
    }
  },
  created() {
    this.setAxios()
    this._threads = this.threads
  },
  destroyed() {
    this.clearFiles()
  },
  methods: {
    handleFileChange(e) {
      const files = e.target.files
      if (!files) return
      this.fileIndex = 0 // 重置文件下标
      // 判断文件选择的个数
      if (this.limit && files.length > this.limit) {
        this.$message.warning(
          `最多支持选择${this.limit}个文件`
        )
        return
      }
      if (this.limit && this.uploadFiles.length >= this.limit) {
        this.$message.warning(
          `最多支持选择${this.limit}个文件`
        )
        return
      }
      Array.prototype.slice.call(files).forEach((item) => {
        this.handleStart(item)
      })
      this.status = Status.read
    },

    async handleStart(file) {
      // 初始化部分自定义属性
      file.status = fileStatus.wait
      file.chunks = []
      file.uploadProgress = 0
      file.fakeUploadProgress = 0 // 假进度条，处理恢复上传后，进度条后移的问题
      file.hashProgress = 0
      if (this.beforeUpload) {
        const before = this.beforeUpload(file)
        if (before && before.then) {
          before.then((res) => {
            this.uploadFiles.push(file)
          })
        }
      }
      if (!this.beforeUpload) {
        this.uploadFiles.push(file)
      }
      const chunknum = Math.ceil(file.size / this.chunkSize)
      const name = file.name
      const size = file.size
      const md5 = await this.calculateHash(this.createFileChunk(file))
      const obj = {
        md5: md5,
        fileName: name,
        fileSize: size,
        chunkNum: chunknum,
        chunkSize: this.chunkSize
      }
      await this.getUUID(obj).then((data) => {
        this.uploadFiles = this.uploadFiles.map((a, index) => {
          if (index === this.fileIndex) {
            a['UUID'] = data
          }
          return a
        })
      })
      this.status = Status.wait
    },

    async handleUpload() {
      if (!this.uploadFiles) return
      this.status = Status.uploading
      const files = this.uploadFiles
      for (let i = 0; i < files.length; i++) {
        this.fileIndex = i
        if (['secondPass', 'success', 'error'].includes(files[i].status)) {
          continue
        }

        const chunks = this.createFileChunk(files[i])

        this.status = Status.uploading

        files[i].status = fileStatus.uploading

        let already = []
        // 获取已经上传成功的切片
        await this.getAlreadyChunk({ UUID: files[i].UUID }).then((data) => {
          already = Object.assign([], data)
        })

        files[i].chunks = chunks.map(({ file }, index) => ({
          UUID: files[i].UUID,
          fileName: files[i].name,
          index,
          chunk: file,
          size: file.size,
          uploaded: already && already.includes(index + ''), // 标识：是否已完成上传
          progress: already && already.includes(index + '') ? 100 : 0,
          status: already && already.includes(index + '') ? 'success' : 'wait' // 上传状态，用作进度状态显示
        }))

        this.$set(files, i, files[i])

        await this.uploadChunks(files[i])
      }
    },

    /**
     * 将切片传输给服务端
     */
    async uploadChunks(data) {
      var chunkData = data.chunks
      return new Promise(async(resolve, reject) => {
        const requestDataList = chunkData
          .filter(({ uploaded }) => !uploaded)
          .map(({ chunk, UUID, fileName, index }) => {
            const formData = new FormData()
            formData.append('UUID', UUID)
            formData.append('file', chunk)
            formData.append('fileIndex', index) // 文件名使用切片的下标
            return { formData, index, fileName, UUID }
          })

        try {
          await this.sendRequest(requestDataList, chunkData)
        } catch (error) {
          this.$message.error('上传失败，请尝试重新上传')
          return
        }

        // 合并切片
        const isUpload = chunkData.some((item) => item.uploaded === false)
        if (isUpload) {
          console.log('存在失败的切片')
        } else {
          // 执行合并
          try {
            await this.mergeRequest(data)
            resolve()
          } catch (error) {
            reject()
          }
        }
      })
    },

    /**
     * 并发处理
     */
    sendRequest(forms, chunkData) {
      var finished = 0
      const total = forms.length
      const that = this
      const retryArr = [] // 数组存储每个文件hash请求的重试次数，做累加 比如[1,0,2],就是第0个文件切片报错1次，第2个报错2次

      return new Promise((resolve, reject) => {
        const handler = () => {
          if (forms.length) {
            // 出栈
            const formInfo = forms.shift()
            const formData = formInfo.formData
            const index = formInfo.index
            const sign = this.getSign({ fileIndex: formData.get('fileIndex'), UUID: formData.get('UUID') })
            instance.post('/API-DOCUMENT/document/file/chunk', formData, {
              onUploadProgress: that.createProgresshandler(chunkData[index]),
              cancelToken: new CancelToken((c) => this.cancels.push(c)),
              timeout: 10000000,
              transformRequest: [(data) => {
                return data
              }],
              headers: {
                'Content-Type': 'multipart/form-data',
                ttsxbwqm: sha256(sign.sign),
                ttsxsjzc: sign.time
              }
            }).then((res) => {
              // 更改状态
              chunkData[index].uploaded = true
              chunkData[index].status = 'success'

              finished++
              handler()
            }).catch((e) => {
              // 若状态为暂停或等待，则禁止重试
              if ([Status.pause, Status.wait].includes(this.status)) return

              if (typeof retryArr[index] !== 'number') {
                retryArr[index] = 0
              }

              // 更新状态
              chunkData[index].status = 'warning'

              // 累加错误次数
              retryArr[index]++

              // 重试3次
              if (retryArr[index] >= this.chunkRetry) {
                return reject('重试失败', retryArr)
              }

              this._threads++ // 释放当前占用的通道

              // 将失败的重新加入队列
              forms.push(formInfo)
              handler()
            })
          }

          if (finished >= total) {
            resolve('done')
          }
        }

        // 控制并发
        for (let i = 0; i < this._threads; i++) {
          handler()
        }
      })
    },

    /**
     * 提交文件获取UUID
     */
    async getUUID(params) {
      return new Promise((resolve, reject) => {
        const sign = this.getSign(params)
        instance.post('/API-DOCUMENT/document/file/insertAttachement', params, {
          timeout: 0,
          headers: {
            ttsxbwqm: sha256(sign.sign),
            ttsxsjzc: sign.time
          }
        }).then((res) => {
          if (res.data.code === 200 || res.data.code === '200') {
            resolve(res.data.data)
          } else {
            this.$message.warning(res.data.msg || '文件读取失败')
            this.clearFiles()
            reject()
          }
        }).catch((err) => {
          this.clearFiles()
          reject(err)
        })
      })
    },

    /**
     * 获取已经上传成功的切片
     */
    async getAlreadyChunk(params) {
      return new Promise((resolve, reject) => {
        const sign = this.getSign(params)
        instance.post('/API-DOCUMENT/document/file/getIndexArray', params, {
          timeout: 0,
          headers: {
            ttsxbwqm: sha256(sign.sign),
            ttsxsjzc: sign.time
          }
        }).then((res) => {
          if (res.data.code === 200 || res.data.code === '200') {
            resolve(res.data.data)
          } else {
            this.$message.warning(res.data.msg || '获取已上传切片失败')
            resolve()
          }
        }).catch((err) => {
          reject(err)
        })
      })
    },
    /**
     * 计算签名
     */
    getSign(params) {
      const timeSta = new Date().Format('yyyyMMddhhmmss')
      const signObj = this.objKeySort(params)
      var count = 0 // 对象属性的个数
      let signStr = ''
      for (const i in signObj) {
        if (signObj.hasOwnProperty(i)) {
          count++
        }
      }
      let index = 0
      for (const key in signObj) {
        index += 1
        signStr += key + '=' + signObj[key] || ''
        if (index !== count) {
          signStr += '|'
        }
      }
      signStr += ':' + timeSta + ':document'
      return { sign: signStr, time: timeSta }
    },
    objKeySort(obj) { // 排序的函数
      var newkey = Object.keys(obj).sort() // 先用Object内置类的keys方法获取要排序对象的属性名，再利用Array原型上的sort方法对获取的属性名进行排序，newkey是一个数组
      var newObj = {} // 创建一个新的对象，用于存放排好序的键值对
      for (var i = 0; i < newkey.length; i++) { // 遍历newkey数组
        newObj[newkey[i]] = obj[newkey[i]] // 向新创建的对象中按照排好的顺序依次增加键值对
      }
      return newObj // 返回排好序的新对象
    },
    /**
     * 通知服务端合并切片
     */
    mergeRequest(data) {
      return new Promise((resolve, reject) => {
        const params = {
          UUID: data.UUID,
          third: sessionStorage.getItem('FILE_SAVE_THIRD_CONFIG') || ''
        }
        const sign = this.getSign(params)
        instance.post('/API-DOCUMENT/document/file/merge', params, {
          timeout: 10000000,
          headers: {
            ttsxbwqm: sha256(sign.sign),
            ttsxsjzc: sign.time
          }
        }).then((res) => {
          if (res.data.code === 200 || res.data.code === '200') {
            data.status = fileStatus.success
            data['id'] = res.data.data.id
            // 如果使用oss 判断是否支持
            if (this.isOss && sessionStorage.getItem('FILE_SAVE_THIRD_CONFIG')) {
              data['ossPath'] = res.data.data.ossPath
            }
            if (res.data.data.totalPages) { // pdf页数
              data['totalPages'] = res.data.data.totalPages
            }
            if (res.data.data.duration) { // 视频、音频时长
              data['duration'] = res.data.data.duration
            }
            // 判断是否所有都成功上传
            this.isAllStatus()
            resolve()
          } else {
            data.status = fileStatus.error
            this.status = Status.wait
            resolve()
          }
        }).catch(() => {
          data.status = fileStatus.error
          reject()
        })
      })
    },

    /**
     * 创建文件切片
     */
    createFileChunk(file, size = this.chunkSize) {
      const list = []
      var count = 0
      while (count < file.size) {
        list.push({
          file: file.slice(count, count + size)
        })
        count += size
      }
      return list
    },

    /**
     * 暂停上传
     */
    handlePause() {
      this.status = Status.pause
      if (this.uploadFiles.length) {
        const currentFile = this.uploadFiles[this.fileIndex]
        currentFile.status = fileStatus.pause
        // 将当前进度赋值给假进度条
        currentFile.fakeUploadProgress = currentFile.uploadProgress
      }
      while (this.cancels.length > 0) {
        this.cancels.pop()('取消请求')
      }
    },

    /**
     * 恢复上传
     */
    handleResume() {
      this.status = Status.uploading
      this.uploadFiles[this.fileIndex].status = fileStatus.resume
      this.handleUpload()
    },

    /**
     * 生成文件 hash（web-worker）
     */
    calculateHash(chunks) {
      return new Promise((resolve) => {
        this.worker = new Worker('./hash.js')
        this.worker.postMessage({ chunks })
        this.worker.onmessage = (e) => {
          const { percentage, hash } = e.data
          if (this.uploadFiles[this.fileIndex]) {
            this.uploadFiles[this.fileIndex].hashProgress = Number(percentage.toFixed(0))
            this.$set(this.uploadFiles, this.fileIndex, this.uploadFiles[this.fileIndex])
          }
          if (hash) {
            resolve(hash)
          }
        }
      })
    },

    /**
     * 切片上传进度
     */
    createProgresshandler(item) {
      return (p) => {
        item.progress = parseInt(String((p.loaded / p.total) * 100))
        this.fileProgress()
      }
    },

    /**
     * 文件总进度
     */
    fileProgress() {
      const currentFile = this.uploadFiles[this.fileIndex]
      if (currentFile) {
        const uploadProgress = currentFile.chunks.map((item) => item.size * item.progress).reduce((acc, cur) => acc + cur)
        const currentFileProgress = parseInt((uploadProgress / currentFile.size).toFixed(2))

        // 真假进度条处理--处理进度条后移
        if (!currentFile.fakeUploadProgress) {
          currentFile.uploadProgress = currentFileProgress
          this.$set(this.uploadFiles, this.fileIndex, currentFile)
        } else if (currentFileProgress > currentFile.fakeUploadProgress) {
          currentFile.uploadProgress = currentFileProgress
          this.$set(this.uploadFiles, this.fileIndex, currentFile)
        }
      }
    },

    /**
     * axios 设置axios参数
     */
    setAxios() {
      if (!this.headers) return
      for (const i in this.headers) {
        instance.defaults.headers.common[i] = this.headers[i]
      }

      // 是否携带cookie
      if (this.withCredentials) {
        instance.defaults.withCredentials = true
      }

      // 设置baseUrl
      if (this.baseUrl) {
        instance.defaults.baseURL = this.baseUrl
      }
    },

    /**
     * 清空文件
     */
    clearFiles() {
      this.fileIndex = 0
      this.handlePause()

      this.worker && this.worker.terminate() // 中断worker
      this._axiosworker && this._axiosworker.terminate() // 中断_axiosworker

      this.status = Status.wait

      Object.assign(this.$data, this.$options.data()) // 重置data所有数据
    },

    /**
     * 隐藏-后台托管
     */
    handleHide() {
      this.hide = !this.hide
    },
    /**
     * 关闭
     */
    handleClose() {
      this.$emit('close')
    },

    /**
     * 判断是否都已完成上传
     */
    isAllStatus() {
      const isAllSuccess = this.uploadFiles.every((item) => ['success', 'secondPass', 'error'].includes(item.status))
      if (isAllSuccess) {
        this.status = Status.done
        const arr = []
        this.uploadFiles.forEach(a => {
          arr.push(a.id)
        })
        this.$emit('success', arr.join(','), this.uploadFiles)
      }
    }
  }
}
</script>
<style scoped lang="scss">
.browser-upload-main {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 3000000234;
  .main-mask {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    background: rgba(0, 0, 0, 0.4);
    transform: translate3d(0, 0, 0);
    transition: transform ease 1ms;
    z-index: 9;
  }

  .main-small {
    position: fixed;
    right: 30px;
    bottom: -50px;
    width: 44px;
    height: 44px;
    line-height: 44px;
    font-size: 22px;
    box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12),
      0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
    color: #000;
    background-color: #fff;
    border-radius: 50%;
    z-index: 2;
    text-align: center;
    animation: moveout 0.5s;
    cursor: pointer;
    > i {
      font-size: 28px;
    }
  }

  .main-inner {
    position: absolute;
    top: 0;
    left: 50%;
    bottom: auto;
    width: 800px;
    transform: translate3d(-50%, 100px, 0);
    transition: transform ease 0.5s;
    border: 1px solid #d2d2d2;
    border-radius: 4px;
    background-color: #fff;
    padding-bottom: 15px;
    padding: 10px;
    max-height: 500px;
    overflow: hidden;
    z-index: 10;
    .inner-hide {
      position: absolute;
      top: 5px;
      right: 5px;
      z-index: 2;
      > i {
        font-size: 18px;
        cursor: pointer;
      }
    }

    .inner-progress {
      margin-bottom: 15px;
      .btns {
        position: relative;
        .select-file-input {
          position: absolute;
          display: inline-block;
          left: 0;
          top: 0;
          border: none;
          opacity: 0;
          width: 96px;
          height: 28px;
        }
      }
    }

    .inner-list {
      .list-item {
        padding: 8px 10px;
        display: flex;
        justify-content: center;
        justify-items: center;
        line-height: 25px;
        position: relative;
        &:hover .item-chunk-box {
          display: block;
        }
        div {
          flex: 1;
        }
        .item-name {
          overflow: hidden;
          text-overflow: ellipsis;
          white-space: nowrap;
          margin-right: 6px;
          .svg-icon {
            font-size: 22px;
            vertical-align: sub;
          }
        }
        .item-size {
          overflow: hidden;
          text-overflow: ellipsis;
          white-space: nowrap;
        }
        .item-status {
          flex: 0 0 10%;
          text-align: center;
          text-align: left;
          .el-icon-circle-check {
            color: #67c23a;
          }
          .el-icon-circle-close {
            color: #f00;
          }
        }
        .item-chunk-box {
          display: none;
          transition: all 3s;
          position: absolute;
          top: 0;
          left: 40px;
          z-index: 10;
        }
        .item-progress {
          flex: 0 0 60%;
          display: flex;
          align-items: center;
        }
      }
    }
  }

  &.hide {
    .main-mask {
      transform: translate3d(0, -100%, 0);
    }

    .main-small {
      bottom: 30px;
      animation: movein 0.5s;
    }

    .main-inner {
      transform: translate3d(-50%, -500%, 0);
    }
  }

  .progress-box {
    width: 100%;
  }

  .upload-tip {
    font-size: 12px;
    color: #606266;
    margin-top: 7px;
  }

  .el-progress {
    width: 80%;
    display: inline-block;
  }

  >>> .el-collapse-item__header {
    height: auto;
  }
  .linear-btn {
    animation: flicker 2s infinite;
    &:hover {
      animation: none;
    }
  }
  @keyframes flicker {
    from {
      background-color: $mainColor;
    }
    to {
      background-color: #fff;
    }
  }
  @keyframes movein {
    from {
      bottom: -50px;
    }
    to {
      bottom: 30px;
    }
  }
  @keyframes moveout {
    from {
      bottom: 30px;
    }
    to {
      bottom: -50px;
    }
  }
}
</style>
